package mastermind;

import java.io.EOFException;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import mastermind.game.Game;
import mastermind.game.GameException;
import mastermind.game.Pieces;
import mastermind.game.Player;
import mastermind.game.Score;
import mastermind.net.GameStream;
import mastermind.net.Protocol;

public class Server implements Protocol {
	private Map<String, Game> currentGames;
	private Map<String, String> loggedInPlayers;
	private DataOperator dao;
	
	/**
	 * Default constructor
	 */
	public Server() {
		currentGames = new HashMap<String, Game>();
		loggedInPlayers = new HashMap<String, String>();
		
		dao = new DataOperator();
		try {
			/* listen for connections to the server */
			ServerSocket serverSocket = new ServerSocket(SERVER_DEFAULT_PORT);

			while (true) {
				System.out.println("waiting for connection");
				/* accept a client connection */
				Socket s = serverSocket.accept();

				/* create a thread to handle the client */
				ClientHandler ch = new ClientHandler(s);
				new Thread(ch).start();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Creates a separate stream for each thread
	 * @author batman
	 *
	 */
	class ClientHandler implements Runnable {
		private Socket socket;
		private GameStream conn;

		public ClientHandler(Socket socket) throws IOException {
			this.socket = socket;
			conn = new GameStream(socket);
		}
		
		// main switch in the program, designates which function to take
		public void run() {
			try {
				OpCode o;
				while (!socket.isClosed()) {
					System.out.println("waiting for opCode");
					o = conn.readOpCode();
					System.out.println("opCode is: " + o);
					switch (o) {
					case REGISTER:
						register();
						break;
					case LOGIN:
						login();
						break;
					case START_GAME:
						startGame();
						break;
					case GUESS:
						guess();
						break;
					case GET_STATS:
						getStats();
						break;
					case GET_LEADERBOARD:
						getLeaderBoard();
						break;
					case LOGOUT:
						logout();
						break;
					default:
						conn.sendOpCode(OpCode.UNRECOGNISED_OPERATION);
						System.err.println("unrecognized operation");
					}
				}
			} catch (EOFException e) {
				System.err.println("nothing more to read");
			} catch (IOException e) {
				e.printStackTrace();
			}
			System.out.println("server thread terminating");
		}
		
		/**
		 * Internal method used to register a player
		 * @throws IOException
		 */
		private void register() throws IOException {
			try {
				Player registrant = (Player) conn.readObject();
				// must supply a minimum of user name
				if(registrant.getUsername().isEmpty()) {
					System.err.println("register: require username");
					conn.sendOpCode(OpCode.FAILURE);
					return;
				}
				
				if (!dao.userExists(registrant.getUsername())) {
					// attempt to write out the file
					boolean playerStatus = dao.addPlayer(registrant);

					// inform client of registration status
					if (playerStatus) {
						System.out.println("registered: "
								+ registrant.getUsername());
						conn.sendOpCode(OpCode.SUCCESS);
					} else {
						System.err.println("File write error...");
						conn.sendOpCode(OpCode.FAILURE);
					}
				} else {
					System.out.println("Duplicate user");
					conn.sendOpCode(OpCode.FAILURE);
				}
			} catch (ClassNotFoundException e) {
				System.err.println("didn't receive a player object");
			} finally {
				conn.out.flush();
			}
		}
		
		/**
		 * Internal method used to login a player
		 * @throws IOException
		 */
		private void login() throws IOException {
			// read details from client
			String username = conn.in.readUTF();
			String password = conn.in.readUTF();
			
			// if player already logged in, don't let them again
			if(loggedInPlayers.containsKey(username)) {
				System.err.println(username + " already logged in");
				conn.sendOpCode(OpCode.FAILURE);
				return;
			} 
			
			// check validity
			Player p = dao.getPlayer(username);
			if(p != null && p.getPassword().equals(password)) {
				String sessionKey = UUID.randomUUID().toString();
				synchronized (loggedInPlayers) {
					loggedInPlayers.put(username, sessionKey);
				}
				if (dao.hasSaveGame(username)) {
					Game g = dao.getSavedGame(username);
					// add to current games
					synchronized (currentGames) {
						if (g != null) {
							currentGames.put(username, g);
						}
					}
					if (g != null) {
						conn.sendOpCode(OpCode.RESTORE_GAME);
						conn.out.writeUTF(sessionKey);
						conn.oout.writeObject(g.getGuesses());
						return;
					}
				}
				conn.sendOpCode(OpCode.SUCCESS);
				conn.out.writeUTF(sessionKey);
			} else {
				conn.sendOpCode(OpCode.FAILURE);
			}
		}
		
		/**
		 * Internal method that starts a game for a user
		 * @throws IOException
		 */
		private void startGame() throws IOException {
			// expect a username & session token
			String username = conn.in.readUTF();
			String key = conn.in.readUTF();
			
			
			// make sure player is logged in & has valid key
			if (!validSession(username, key)) {
				conn.sendOpCode(OpCode.FAILURE);
				return;
			}
						
			// if player is in game and request a new one, end current game
			if (currentGames.containsKey(username)) {
				endGame(username);
			}
			
			// create a new game
			Game g = new Game();
			synchronized (currentGames) {
				currentGames.put(username, g);	
			}
			conn.sendOpCode(OpCode.SUCCESS);
			System.out.println("started game for player " + username);
		}
		
		/**
		 * Internal method used to end a 'current' players game
		 * @param username
		 */
		private void endGame(String username) {
			// make sure player actually is in game
			if(currentGames.containsKey(username)) {
				// get score for the game
				Game g = currentGames.get(username);
				Score score = g.getScore();
				
				// record score
				dao.addScore(username, score);
				
				// remove from list of active games
				synchronized (currentGames) {
					currentGames.remove(username);	
				}
				
				// if game had a save state, remove it
				if(dao.hasSaveGame(username)) {
					dao.deleteSaveGame(username);
				}
			}
		}
		
		/**
		 * Internal method used to send/receive guess feedback
		 * @throws IOException
		 */
		private void guess() throws IOException {
			try {
				String username = conn.in.readUTF();
				String key = conn.in.readUTF();
				Pieces p = (Pieces) conn.readObject();
				
				// make sure player is logged in & has valid key
				if (!validSession(username, key)) {
					conn.sendOpCode(OpCode.FAILURE);
					return;
				}
				
				// we want the users current game
				Game g = currentGames.get(username);
				if(g == null) {
					conn.sendOpCode(OpCode.END_GAME);
					conn.out.writeUTF("game is over!");
				}
				
				if(!g.haveGuesses()) {
					endGame(username);
				}
					
				String feedback = g.makeGuess(p);
				
				// send confirmation of guess and feedback
				conn.sendOpCode(OpCode.SUCCESS);
				conn.out.writeUTF(feedback);
				
				// if feedback was all b, end game
				if (Game.correctGuess(feedback)) {
					System.out.println("correct guess, ending game");
					endGame(username);
				}
			} catch (ClassNotFoundException e) {
				conn.sendOpCode(OpCode.FAILURE);
				conn.out.writeUTF("invalid guess");
			} catch (GameException e) {
				conn.sendOpCode(OpCode.FAILURE);
				conn.out.writeUTF(e.getMessage());
			}
		}
		
		/**
		 * Internal method used to retrieve a user's average
		 * @throws IOException
		 */
		private void getStats() throws IOException {
			String username = conn.in.readUTF();
			String key = conn.in.readUTF();
			
			// make sure player is logged in & has valid key
			if (!validSession(username, key)) {
				conn.sendOpCode(OpCode.FAILURE);
				return;
			}
			conn.sendOpCode(OpCode.SUCCESS);
			conn.out.writeDouble(dao.getPlayerAverage(username));
		}
		
		/**
		 * Internal method used to retrieve a 'leader-board' of averages
		 * @throws IOException
		 */
		private void getLeaderBoard() throws IOException {
			conn.sendOpCode(OpCode.SUCCESS);
			conn.oout.writeObject(dao.getLeaderBoard());
			conn.oout.flush();
		}
		
		private void logout() throws IOException {
			// read details from client
			String username = conn.in.readUTF();
			String key = conn.in.readUTF();
			
			// make sure player is logged in & has valid key
			if (!validSession(username, key)) {
				conn.sendOpCode(OpCode.FAILURE);
				return;
			}
			
			// if player is in game, save their game
			if(currentGames.containsKey(username)) {
				Game g = currentGames.get(username);
				if(g.haveGuesses()) {
					dao.saveGame(username, g);
				}
			}
			
			synchronized (loggedInPlayers) {
				loggedInPlayers.remove(username);
			}
			conn.sendOpCode(OpCode.SUCCESS);
			conn.closeAll();
		}

		/**
		 * Internal method used to check session validity
		 * 
		 * @param username
		 *            Player user name
		 * @param key
		 *            Session key
		 * @return Boolean
		 */
		private boolean validSession(String username, String key) {
			return loggedInPlayers.containsKey(username)
					&& loggedInPlayers.get(username).equals(key);
		}
	}

	public static void main(String[] args) {
		System.out.println("starting mastermind server");
		new Server();
	}
}
